package edu.stonybrook.cs.gars.server;

import java.util.ArrayList;

import java.util.List;
import java.util.UUID;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import edu.stonybrook.cs.gars.client.AuthenticationService;
import edu.stonybrook.cs.gars.client.GARS;
import edu.stonybrook.cs.gars.server.data.DataHandler;
import edu.stonybrook.cs.gars.server.security.BCrypt;
import edu.stonybrook.cs.gars.shared.FieldVerifier;
import edu.stonybrook.cs.gars.shared.users.LoggedInUser;
import edu.stonybrook.cs.gars.shared.users.Role;
import edu.stonybrook.cs.gars.shared.users.Subject;
import edu.stonybrook.cs.gars.shared.users.User;

/**
 * The server side implementation of the RPC service.
 */
@SuppressWarnings("serial")
public class AuthenticationServiceImpl extends RemoteServiceServlet implements
		AuthenticationService {

	private DataHandler dataHandler = DataHandler.getInstance();
	DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

	@Override
	public LoggedInUser authenticate(String username, String password)
			throws IllegalArgumentException 
	{
		Entity userLoggingIn = this.retrieveUserRecordByUsername(username);
		
		// Check if this user exists in the database
		if (!(userLoggingIn == null)) 
		{
			String hashedPassword = (String) userLoggingIn.getProperty("Password");
			
			// Verify password for user
			if (BCrypt.checkpw(password, hashedPassword)) {
				
				LoggedInUser loggedInUser = this.createLoggedInUser(userLoggingIn);
				
				dataHandler.addLoggedInUser(loggedInUser);

				return loggedInUser;
			}
			else 
			{
				//for security purposes we don't let people know that they have a
				// valid username unless they have
				// the password associated with that username
				throw new IllegalArgumentException(
						"Invalid username or password.");
			}
		} 
		else 
		{
			writeUser(username, password);
			
			LoggedInUser loggedInUser = this.createLoggedInUser(this.retrieveUserRecordByUsername(username));
			
			return loggedInUser;
			
		}

		// TODO server side field validation
		/*
		 * // Verify that the input is valid. if
		 * (!FieldVerifier.isValidName(input)) { // If the input is not valid,
		 * throw an IllegalArgumentException back to // the client. throw new
		 * IllegalArgumentException( "Name must be at least 4 characters long");
		 * }
		 */

		// TODO Sanitize data before interacting with the database
		/*
		 * // Escape data from the client to avoid cross-site script
		 * vulnerabilities. escapedUsername = escapeHtml(input); userAgent =
		 * escapeHtml(userAgent);
		 */

		/*
		 * if (!username.equals("") && !password.equals("")) {
		 * System.out.println("Username: " + username + " Password: " +
		 * password); return UUID.randomUUID().toString(); } else { if
		 * (username.equals("") && password.equals("")) throw new
		 * IllegalArgumentException(
		 * "Username cannot be blank<br />Password cannot be blank"); if
		 * (username.equals("")) throw new
		 * IllegalArgumentException("Username cannot be blank"); else throw new
		 * IllegalArgumentException("Password cannot be blank"); } return
		 * "failure";
		 */
	}
	
	private Entity retrieveUserRecordByUsername(String username)
	{
		Query query = new Query("User", KeyFactory.createKey("Username", username));
		PreparedQuery preparedQuery = datastore.prepare(query);
		return preparedQuery.asSingleEntity();
	}
	
	private LoggedInUser createLoggedInUser(Entity userLoggingIn)
	{
		return new LoggedInUser(
				UUID.randomUUID().toString(),
				(String) userLoggingIn
				.getProperty("Username"),
				"",
		User.roleStringToEnum((String) userLoggingIn
				.getProperty("Role")),
		((Long) userLoggingIn
				.getProperty("Number of Assigned Reviews"))
				.intValue(),
		((Double) userLoggingIn
				.getProperty("Workload"))
				.floatValue(),
		((Boolean) userLoggingIn
				.getProperty("Available"))
				.booleanValue(),
		(String) userLoggingIn
				.getProperty("Email"),
		(List<String>) userLoggingIn
				.getProperty("Countries"),
		User.fieldsStringListToFieldsEnumVector((List<String>) userLoggingIn
				.getProperty("Fields")));
	}

	private void writeUser(String username, String password) {
		Entity login = new Entity("User", KeyFactory.createKey("Username",
				username));
		List<String> countries = new ArrayList<String>();
		countries.add("United States of America");
		countries.add("Canada");
		List<String> fields = new ArrayList<String>();
		fields.add(Subject.AI.toString());
		fields.add(Subject.NET.toString());

		login.setProperty("Username", username);
		login.setProperty("Password", BCrypt.hashpw(password, BCrypt.gensalt()));
		login.setProperty("Role", Role.CHAIR.toString());
		login.setProperty("Number of Assigned Reviews", 0);
		login.setProperty("Workload", 1.0);
		login.setProperty("Available", true);
		login.setProperty("Email", "chair@cs.stonybrook.edu");
		login.setProperty("Countries", countries);
		login.setProperty("Fields", fields);

		datastore.put(login);
	}

	/**
	 * Escape an html string. Escaping data received from the client helps to
	 * prevent cross-site script vulnerabilities.
	 * 
	 * @param html
	 *            the html string to escape
	 * @return the escaped string
	 */
	private String escapeHtml(String html) {
		if (html == null) {
			return null;
		}
		return html.replaceAll("&", "&amp;").replaceAll("<", "&lt;")
				.replaceAll(">", "&gt;");
	}

}
